Skip to content

refactor(cli): render database history through the shared status table renderer - #1206

Merged
aparajon merged 3 commits into
mainfrom
armand/history-status-columns
Aug 31, 2026
Merged

refactor(cli): render database history through the shared status table renderer#1206
aparajon merged 3 commits into
mainfrom
armand/history-status-columns

Conversation

@aparajon

@aparajon aparajon commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator

Why this matters

#1062 gave the status list a real column renderer — optional columns that drop when no row fills them, dash-fill for missing values, width sizing, state coloring — but it was typed to ActiveApplyData, so the history table next to it still hand-rolls its layout with five parallel maxLen width variables and a positional fmt.Printf that must be kept in sync by hand. Every column added to either table means re-deriving alignment logic in a second place, and the two tables had already drifted on a detail (where the color escape closes relative to the column separator). This PR makes the renderer generic over the row type and moves the history table onto it, with no change to what operators see.

What it does

statusColumn and its helpers (retainPopulatedStatusColumns, statusColumnValue, statusColumnWidths) become generic over the row type, and a new writeStatusTable owns the shared header+rows loop, taking a row-state accessor so colored columns work for any row struct. WriteDatabaseHistory now just declares its columns.

Before
  WriteStatusList       ──► statusColumn{value func(ActiveApplyData)}
  │                         └─ inline header/row loop, width sizing, coloring
  WriteDatabaseHistory  ──► maxID/maxEnv/maxState/maxStarted/maxDur
                            └─ positional fmt.Printf layout, own coloring

After
  WriteStatusList       ──► statusListColumns()      ─┐
                                                      ├─► writeStatusTable[Row]
  WriteDatabaseHistory  ──► databaseHistoryColumns() ─┘     ├─ widths: header vs widest cell
                                                            ├─ dash for a value a row is missing
                                                            └─ state-colored cell, escape closed
                                                               before the column separator

Three rendering properties worth calling out:

  • History output is byte-identical for every populated cell, pinned by exact-bytes tests (fixed clocks, full-escape-sequence expected strings) covering colored states, an unrecognized state, missing timestamps, and the empty-database message. The one divergence is deliberate and pinned: a cell whose apply recorded no value — an empty caller or state — now renders - where the old layout emitted blank padding, the same "none recorded" rendering the status list already uses.
  • The status list adopts the history table's escape placement: a colored state cell now closes its ANSI escape before the two-space separator instead of after. That renders identically on any terminal — the escapes are foreground-only — and TEMPLATES.md regenerates with no diff. An exact-byte test pins the new composition on the status list itself.
  • Columns are sized in terminal cells, not bytes: widths come from ui.VisibleWidth and padding from ui.PadVisible, so a multi-byte value — a non-ASCII environment name, a state glyph — can no longer misalign every column to its right. All current cells are ASCII, so today's output is unchanged; a CJK-name test pins the alignment.

How it moves us toward the northstar

Operator CLI surfaces are growing tables faster than they're growing layouts — deployment-filtered lists, remote-handle columns, history. With one shared renderer, the next table (and the next column on an existing one) is a column declaration, not a second alignment implementation, so the fleet CLI keeps one consistent table behavior as it expands.

Opened by Claude (Fable 5).

…e renderer

Generalize statusColumn and its helpers over the row type so any CLI table
can define columns against its own row struct, add writeStatusTable as the
single header+rows renderer, and replace WriteDatabaseHistory's hand-rolled
column layout with history-specific statusColumn definitions. History output
is byte-identical, pinned by an exact-bytes test; the status list's colored
state cell now ends its color escape before the column separator, which
renders identically.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 29, 2026 16:48

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors the CLI table rendering in templates to reuse a single shared “status table” renderer across multiple outputs, starting with the database history view, while preserving the existing user-facing formatting (including ANSI coloring behavior).

Changes:

  • Generalizes statusColumn and related helpers over an arbitrary row type and introduces writeStatusTable to own the shared header/row rendering loop (including correct ANSI reset behavior around separators).
  • Refactors WriteStatusList and WriteDatabaseHistory to render via writeStatusTable, replacing bespoke printing/width tracking in history output.
  • Adds exact-bytes tests for the database history table output (including unknown-state and missing-timestamp cases) plus an empty-history test.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
pkg/cmd/internal/templates/progress.go Introduces generic status-table primitives (statusColumn[T], writeStatusTable) and refactors status list + database history rendering to use them.
pkg/cmd/internal/templates/progress_states_test.go Adds exact-bytes tests pinning database history table output and the empty-history message.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@aparajon
aparajon marked this pull request as ready for review August 30, 2026 13:46
@Kiran01bm

Copy link
Copy Markdown
Collaborator

🤖 Review findings - created by Kiran's code review agent - for schemabot/pull/1206, 1329361.

Verdict: 3 findings — no blockers; 1 non-blocking (the refactor silently changes a second surface), 2 suggestions.

Non-blocking

  1. The refactor changes WriteStatusList's bytes too, which the PR never mentions. Previously statusCell baked the two-space separator into the cell (fmt.Sprintf("%-*s ", width, value)) and colorFn wrapped the whole thing, so the STATE cell emitted ESC[32mCompleted␣␣␣␣␣␣␣ESC[0m. Now statusCell drops the separator and writeStatusTable prints it after the escape, giving ESC[32mCompletedESC[0m␣␣ — every status-list row with a recognized state changes. The two surfaces disagreed before (history already put the separator outside), and unifying them is the right call, but the PR is titled and described as a history change, adds byte-exact tests only for history, and leaves the surface it actually altered unpinned.

General suggestions

  1. History is not byte-identical as the PR body claims. Routing through statusColumnValue substitutes - for an empty ApplyID, Environment, State or Caller, where the removed hand-rolled Printf emitted a blank padded cell. This is reachable, not theoretical — caller is varchar(255) NOT NULL DEFAULT '' and resolveCaller returns the request caller unchanged when there is no authenticated subject, and both applySource("") and state.Label("") return "". The new dash is the better rendering and matches the status list; the issue is that TestWriteDatabaseHistoryTable only exercises missing timestamps, which already produced - on both sides, so the one genuinely divergent input is untested.

  2. The new shared renderer bakes in byte-width. statusColumnWidths measures with len(), so the moment any cell carries a multi-byte value — a non-ASCII database or environment name, or a state glyph if these tables ever gain one — every column right of it misaligns. ui.VisibleWidth / ui.PadVisible exist for exactly this and already handle emoji-variation sequences; this is the natural moment to adopt them, since the whole point of the change is to make one renderer the shared path. No live bug today: every current cell is ASCII.

The one thing that could have broken, verified

Whether generifying statusColumn silently changed which columns the status list drops. It did not: retainPopulatedStatusColumns and anyStatusRowFillsColumn are the same logic with ActiveApplyData replaced by a type parameter, statusListColumns still calls it, and the optional/unconditional split for EXTERNAL ID is untouched. The history path deliberately bypasses it — WriteDatabaseHistory calls databaseHistoryColumns() directly and none of those columns are optional, so no history column can ever be dropped, matching the old renderer which had no dropping at all. Header-row bytes are also unchanged on both surfaces: the old and new forms both dim the entire row including separators.

Verified correct

  • Column-width seeds are identical: the old hand-rolled minimums (8/3/5/7/8) are exactly len(header) for APPLY ID / ENV / STATE / STARTED / DURATION.
  • formatStartedAt("") and formatApplyDuration("", …) already returned - themselves, so those cells are genuinely unchanged.
  • writeFailedStatusList renders per-apply blocks, not a table, so there is no third surface that should have been unified and was missed.
  • The last: true column correctly suppresses both the padding and the trailing separator, so no row gains trailing whitespace.
  • stateColorFunc is foreground-only, so finding 1 is a byte change with no visual difference in a terminal.
  • go test -count=1 ./pkg/cmd/internal/templates/ passes at this head, and all CI checks are green — consistent with no existing test having pinned the status-list colored bytes.

This review was generated by Claude Code (claude-opus-5).

aparajon and others added 2 commits August 31, 2026 11:52
The shared status renderer sized columns with len() and padded with
printf, so any multi-byte cell — a non-ASCII database or environment
name, a state glyph — would misalign every column to its right. Widths
now come from ui.VisibleWidth and padding from ui.PadVisible, which
already handle escapes and emoji-presentation sequences.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The status list's colored STATE cell closes its color escape before the
two-space separator, the history table stands a dash in for a cell whose
apply recorded no value, and column padding counts terminal cells so a
CJK name aligns with its ASCII neighbors — each now pinned by exact-byte
assertions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@aparajon

Copy link
Copy Markdown
Collaborator Author

🤖 Addressed all three findings in two new commits — a8384f6b (renderer) and 0e90a0e6 (test pins):

  1. Pinned in 0e90a0e. TestWriteStatusListColoredStateCellBytes asserts the status list's exact colored-cell bytes: a padded state stays inside the escape (ESC[36mRunning··ESC[0m··) and the separator outside. Mutation-checked — moving the padding outside the escape (visually identical, since PadVisible ignores escapes when measuring) fails the test. The PR body's rendering notes now also say the surface is pinned, not just changed.

  2. Pinned in 0e90a0e, body corrected. The exact-bytes history test gains an apply that recorded neither state nor caller, pinning the - in every empty cell (apply_bare staging - - - -). The body no longer claims byte-identical output; it names the one deliberate divergence — dash instead of blank padding for a value the apply never recorded, matching the status list — and that it's pinned. Mutation-checked (dropping the dash substitution fails the test).

  3. Adopted in a8384f6. statusColumnWidths measures with ui.VisibleWidth and statusCell pads with ui.PadVisible, so the shared renderer sizes columns in terminal cells. TestWriteDatabaseHistoryAlignsMultiByteCells pins a CJK environment name aligning with its ASCII neighbors; both halves are mutation-checked independently (byte-counted widths and byte-counted padding each fail it on a different assertion). All current cells are ASCII, so today's output is unchanged — TEMPLATES.md regenerates with zero diff from a fresh binary.

Verification: go test -race ./pkg/cmd/... green, TEMPLATES.md byte-identical, five mutants killed across the three findings.

This reply was generated by Claude Code (Claude Fable 5).

@aparajon
aparajon merged commit 36846d0 into main Aug 31, 2026
37 checks passed
@aparajon
aparajon deleted the armand/history-status-columns branch August 31, 2026 16:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants